import net from "net"
import MB from "modbus-serial"
import { WebSocket, CloseEvent, ErrorEvent, Event } from "ws"
import { type BaseType, type cfgDevice, type cfgServer, type cfgTag, getconfig, configfile } from "./cfg";
import { type Log4js, type Logger } from "log4js"


let log: Logger;

type Message = {
    cmd: string,
    pt: any;
}

enum RegistType {
    Coil = 0,
    Discrete = 1,
    Input = 3,
    Hold = 4
}
function bcd2number(buf: Uint8Array): number {
    let result: number = 0;
    for (let i of buf) {
        result *= 10;
        result += i >> 4;
        result *= 10;
        result += i & 0xf;
    }
    return result;
}
function sbcd2number(buf: Uint8Array): number {
    let sign = buf[0] == 0x00 ? 1 : -1;
    return bcd2number(buf.subarray(1)) * sign;
}
class MTag {
    name: string;
    addr: string;
    phyaddr: number;
    datatype: BaseType;
    regtype: RegistType;
    readonly: boolean;
    precision?: number;
    type: 'string' | 'number' | 'boolean';
    private _value?: boolean | string | number
    bytelen: number;
    phylen: number;
    owner: Device;
    timeStamp!: number;
    private decode_type() {
        switch (this.datatype) {
            case "boolean": return "boolean";
            case "string": return "string";
            default: return "number"
        }
    }
    private get_bytes(len?: number) {
        switch (this.datatype) {
            case "bcd":
            case "sbcd":
            case "string":
                return (len! + 1) & (~1);
            case "double":
                return 8;
            case "dword":
            case "float":
            case "long":
                return 4;
            case "boolean":
                return 1;
            default:
                return 2;
        }
    }
    private get_phylen() {
        if (this.regtype < 3) {
            return this.bytelen;
        } else {
            return this.bytelen >> 1;
        }
    }
    from_dev(buf: Buffer, offset: number): string | number | boolean {
        offset = this.regtype == RegistType.Coil || this.regtype == RegistType.Discrete ? Math.floor(offset / 8) : offset;
        buf = buf.subarray(offset, offset + this.bytelen)

        switch (this.datatype) {
            case "bcd":
                return bcd2number(buf);
            case "sbcd":
                return sbcd2number(buf);
            case "string":
                let len = buf.indexOf(0);
                return len > 0 ? buf.toString("utf8", 0, len) : buf.toString("utf8");
            case "double":
                return buf.readDoubleBE();
            case "float":
                return buf.readFloatBE();
            case "long":
                return buf.readInt32BE();
            case "dword":
                return buf.readUint32BE();
            case "short":
                return buf.readInt16BE();
            case "word":
                return buf.readUint16BE();
            case "boolean":
                const shift = offset % 8;
                return Boolean(buf.readUint8() & (1 << shift));
        }
    }
    write2Buf(buf: Buffer, offset: number) {
        buf = buf.subarray(offset, offset + this.bytelen);
        switch (this.datatype) {
            case "boolean":
                buf.writeUint8(Number(this.value));
                break;
            case "bcd":
            case "sbcd":
                break;
            case "string":
                buf.write(this.value as string, "utf8");
                break;
            case "double":
                buf.writeDoubleBE(this.value as number);
                break;
            case "float":
                buf.writeFloatBE(this.value as number);
                break;
            case "dword":
                buf.writeUInt32BE(this.value as number);
                break;
            case "long":
                buf.writeInt32BE(this.value as number);
                break;
            case "word":
                buf.writeUint16BE(this.value as number);
                break;
            case "short":
                buf.writeInt16BE(this.value as number);
        }
    }
    get value(): string | number | boolean | undefined {
        return this._value;
    }
    set value(v) {
        if (v === undefined || this.readonly || this._value === v) return;
        this._value = v;
    }
    update(buffer: Buffer, offset: number) {
        let v = this.from_dev(buffer, offset);
        if (this.precision && typeof v == "number") {
            v = +(v.toFixed(this.precision))
        }
        if (this._value != v) {
            log.trace(`Tag ${this.addr} Changed. new value is:${v}`)
            this.timeStamp = Date.now();
            this._value = v;
            this.owner.notify(this);
        }
    }
    constructor(owner: Device, cfg: cfgTag) {
        this.owner = owner;
        this.regtype = Number(cfg.addr.at(0)) as unknown as RegistType;
        this.addr = cfg.addr;
        this.precision = cfg.precision;
        this.datatype = cfg.datatype;
        this.bytelen = this.get_bytes(cfg.length);
        this.phylen = this.get_phylen();
        this.name = cfg.name;
        this.readonly = cfg.readonly || this.regtype == 1 || this.regtype == 3 ? true : false;
        this.phyaddr = +this.addr.slice(1) - 1
        this.type = this.decode_type()
    }
    toJSON() {
        return { name: this.name, type: this.type, value: this._value, quality: 192, timeStamp: this.timeStamp, readonly: this.readonly }
    }
}
class Group {
    registtype: RegistType;
    startaddr: number;
    phylen!: number;
    tags: Set<{ tag: MTag, offset: number }>;
    el_size: number;
    get bytelen() {
        return this.phylen * this.el_size;
    }
    constructor(registtype: RegistType, startaddr: number) {
        this.registtype = registtype;
        this.el_size = this.registtype < 3 ? 1 : 2;
        this.startaddr = startaddr;
        this.tags = new Set();
    }
}
class ConnectServerError {
    message: string
    name: string
    constructor(msg: string) {
        this.message = msg
        this.name = this.constructor.name;
    }
}
class Device {
    mb: MB;
    addr: number
    ws?: WebSocket;
    groups: Set<Group>;
    children: { [key: string]: MTag };
    stop: boolean;
    scanrate: number;
    connected: boolean;
    owner: Server;
    _key: string;
    private regist() {
        const cmd = {
            cmd: "create",
            pt: Object.values(this.children),
        }
        const json = JSON.stringify(cmd);
        this.ws!.send(json);
    }
    private grouping(tags: MTag[]): Set<Group> {
        let groups: Set<Group> = new Set();
        tags.sort((a, b) => a.regtype == b.regtype ? a.phyaddr - b.phyaddr : a.regtype - b.regtype);
        let pre: MTag | undefined = undefined;
        let grp!: Group;
        let endaddr = 0;
        tags.forEach((tag) => {
            if (!pre || pre.phyaddr + pre.phylen < tag.phyaddr || pre.regtype != tag.regtype) {
                endaddr = tag.phyaddr;
                grp = new Group(tag.regtype, tag.phyaddr)
                groups.add(grp);
            }
            endaddr = Math.max(tag.phyaddr + tag.phylen, endaddr);
            grp.phylen = endaddr - grp.startaddr;
            grp.tags.add({ offset: (tag.phyaddr - grp.startaddr) * grp.el_size, tag: tag });
            pre = tag;
        })
        return groups;
    }
    async scan() {
        this.mb.setID(this.addr);
        for (let grp of this.groups) {
            let buf: Buffer;
            switch (grp.registtype) {
                case RegistType.Coil:
                    buf = (await this.mb.readCoils(grp.startaddr, grp.phylen)).buffer;
                    break;
                case RegistType.Discrete:
                    buf = (await this.mb.readDiscreteInputs(grp.startaddr, grp.phylen)).buffer;
                    break;
                case RegistType.Input:
                    buf = (await this.mb.readInputRegisters(grp.startaddr, grp.phylen)).buffer
                    break;
                case RegistType.Hold:
                    buf = (await this.mb.readHoldingRegisters(grp.startaddr, grp.phylen)).buffer;
                    break;
            }
            grp.tags.forEach(tag => tag.tag.update(buf, tag.offset));
        }
    }
    run() {
        const cb = async () => {
            await this.scan().then(() => {
                setTimeout(cb, this.scanrate);
            }).catch((r) => {
                if (this.mb.isOpen) {
                    log.error("Modbus polling data error. Device Key:%s, Message:%s", this._key, r.message);
                    this.close();
                    this.owner.unregist(this);
                }
            })
        }
        setTimeout(cb, this.scanrate);
    }
    private _notify(): (tag: MTag) => void {
        let pts = new Set();
        let timer: TimerHandler | undefined = undefined;
        return function (this: Device, tag: MTag) {
            if (!this.connected) return;
            pts.add(tag);
            if (!timer) {
                setTimeout(() => {
                    const cmd = {
                        cmd: "notify",
                        pt: Array.from(pts.values()),
                    }
                    const json = JSON.stringify(cmd, ["cmd", "pt", "name", "quality", "timeStamp", "value"]);
                    this.ws!.send(json);
                    timer = undefined;
                    pts.clear();
                }, 1);
            }
        }
    }
    notify: (tag: MTag) => void;
    constructor(owner: Server, socket: net.Socket, cfg: cfgDevice, key: string) {
        this.owner = owner;
        this.scanrate = cfg.scanrate;
        this.connected = false;
        this.children = {};
        this.addr = cfg.addr;
        this.stop = false;
        this._key = key;
        for (let tag of cfg.tags) {
            const x = new MTag(this, tag);
            this.children[x.name] = x;
        }
        this.notify = this._notify().bind(this);
        this.groups = this.grouping(Object.values(this.children));
        this.mb = new MB();
        this.mb.setTimeout(cfg.timeout);
        this.mb.linkTelnet(socket, {}).then(() => {
            return this.scan()
        }).then(() => {
            this.ws = new WebSocket(cfg.server, { rejectUnauthorized: false });
            let timerhandler: NodeJS.Timeout;
            return new Promise<void>((r, j) => {
                let err: ErrorEvent;
                this.ws!.once("open", () => {
                    this.ws!.onerror = this.wsError.bind(this);
                    this.ws!.onclose = this.wsClose.bind(this);
                    this.ws!.onmessage = (event) => this.wsMessage(JSON.parse(event.data.toString("utf-8")))
                    this.regist();
                    this.connected = true;
                    r();
                });
                this.ws!.onerror = e => err = e;
                this.ws!.onclose = () => {
                    j(new ConnectServerError(err.message))
                }
            })
        }).then(() => this.run()
        ).catch(r => {
            switch (r.name) {
                case 'ConnectServerError':
                    log.error("Connect OpcServer Error. Message:%s", r.message);
                    break;
                case 'TransactionTimedOutError':
                    log.error("Modbus polling data error. Device Key:%s, Message:%s", this._key, r.message);
                    break;
                default:
                    log.error("Unknow error. Message:%s", r.message);
            }
            this.close();
            this.owner.unregist(this);
        });

    }
    wsError(event: ErrorEvent) {
        log.error("OPCServer Communication error. Message:%s", event.message);
        this.mb.close(undefined as any);
    }
    wsClose(event: CloseEvent) {
        log.error("Disconnected from the OPCServer.");
        this.close();
        this.owner.unregist(this);
    }
    wsMessage(msg: Message) {

    }
    close() {
        if (this.mb.isOpen) {
            this.mb.destroy(undefined as any);
        }
        if (this.ws && this.ws.readyState == WebSocket.OPEN) {
            this.ws.onclose = null;
            this.ws.onerror = null;
            this.ws.onmessage = null;
            this.ws.close();
        }
    }
}

class Server {
    clients: Set<Device>;
    server: net.Server;
    port: number;
    devices?: { [key: string]: cfgDevice };
    cfg: cfgServer;
    unregist(dev: Device) {
        this.clients.delete(dev);
    }
    regist(dev: Device) {
        this.clients.add(dev);
    }
    onConnect(client: net.Socket) {
        if (!this.devices) {
            client.destroy();
            return;
        }
        client.once("data", d => {
            const key = d.toString("utf-8");
            log.info("new client:" + key);
            const cfg = this.devices![key];
            if (!cfg) {
                client.destroy();
                return;
            }
            const dev = new Device(this, client, cfg, key);
            this.regist(dev);
        })
    }
    onClose() {
        log.info("Server Clocked.")
    }
    onError(err: Error) {
        log.error("Server failure. Message:%s", err.message);
    }
    constructor(cfg: cfgServer) {
        this.port = cfg.port;
        this.cfg = cfg;
        this.clients = new Set();
        this.devices = cfg.devices;
        this.server = net.createServer(this.onConnect.bind(this))
        this.server.on("close", this.onClose.bind(this));
        this.server.on("error", this.onError.bind(this));
    }
    open() {
        this.server.listen(this.port)
    }
    close() {
        this.server.close();
        for (let dev of this.clients) {
            dev.close();
        }
        this.clients.clear();
    }
}

function modbus_open(log4j: Log4js) {
    log = log4j.getLogger("Modbus")
    const cfg = getconfig(configfile);
    const server = new Server(cfg);
    server.open();
}

export {
    modbus_open
}